package ru.sgu.acm.judge.impl;

import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.log4j.Logger;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import ru.sgu.acm.dao.SubmitDao;
import ru.sgu.acm.dao.SubmitDaoImpl;
import ru.sgu.acm.judge.misc.Browser;
import ru.sgu.acm.judge.misc.JudgeHelper;
import ru.sgu.acm.judge.properties.ParametersConfig;
import ru.sgu.acm.misc.Constants;
import ru.sgu.acm.model.Submit;

import javax.xml.xpath.*;
import java.io.IOException;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static ru.sgu.acm.judge.misc.Browser.getHtmlDocumentFromCurrentSession;
import static ru.sgu.acm.judge.misc.Browser.isHttpMethodErrorStatusCode;
import static ru.sgu.acm.judge.misc.RetryingStrategy.isTryOneMoreTime;
import static ru.sgu.acm.judge.misc.RetryingStrategy.startRetryingSection;

/**
 * Generic judge submit file to any online judge.
 * status and submit config files required.
 * If it is neccessary to login --- login config file required. 
 *
 * @author Vitaly Goldshteyn (VitalyGoldstein@gmail.com)
 */
public class GenericOnlineJudge extends AbstractJudge {
    String judgeName;

    HttpClient httpClient;
    XPath xPath = XPathFactory.newInstance().newXPath();

    Logger logger = Logger.getLogger(GenericOnlineJudge.class);

    /**
     * Submit source to the remote judge.
     * @param submit instance for submitting
     * @throws Exception if impossible to submit file for a long time.
     */
    public void submit(Submit submit) throws Exception {
        logger.info("Submitting...");

        Properties submitProperties = getSubmitProperties();
        ParametersConfig config = new ParametersConfig(submitProperties);
        config.setProperty("pseudoId", submit.getProblemPseudoId());
        config.setProperty("source", submit.getSource());
        String ext = submit.getExtension().toLowerCase().replaceAll("\\.", "");
        config.setProperty("extension", ext);

        Map<String, String> problemProperties = submit.getProblemProperties();
        for (Map.Entry<String, String> entry : problemProperties.entrySet()) {
            config.setProperty(entry.getKey(), entry.getValue());
        }

//        GetMethod getMethod = new GetMethod("http://acm.hdu.edu.cn/status.php");
//        httpClient.executeMethod(getMethod);
//        System.out.println(getMethod.getResponseBodyAsString());
        
        PostMethod method = getPostMethod(config);
        try {
            httpClient.executeMethod(method);
            //System.out.println(method.getResponseBodyAsString());
            if (isHttpMethodErrorStatusCode(method.getStatusCode())) {
                throw new IOException("Method return error status code during submitting solution: "
                        + method.getStatusText());
            }
        } catch (IOException e) {
            logger.warn("Unable to send solution to the server, trying again...", e);
            if (isTryOneMoreTime("submit")) {
                submit(submit);
            } else {
                throw(e);
            }
        }
    }

    /**
     * Login to the remote Judge if it is neccessary.
     * @throws Exception if impossible to login for a long time.
     */
    private void login() throws Exception {
        Properties loginProperties = getLoginProperties();
        if (loginProperties == null) {
            return ; //Not neccessary to login.
        }
        logger.info("Loginning...");
        ParametersConfig config = new ParametersConfig(loginProperties);
        PostMethod method = getPostMethod(config);
        try {
            httpClient.executeMethod(method);
            //System.out.println(method.getResponseBodyAsString());
            if (isHttpMethodErrorStatusCode(method.getStatusCode())) {
                throw new IOException("Method return error status code during login: " + method.getStatusText());
            }
            //GetMethod getMethod = new GetMethod("http://acm.hdu.edu.cn/status.php");
            //httpClient.executeMethod(getMethod);
            //System.out.println(getMethod.getResponseBodyAsString());
        } catch (IOException e) {
            logger.warn("Unable to login, tryin again...", e);
            if (isTryOneMoreTime("login")) {
                login();
            } else {
                throw(e);
            }
        }
    }

    private Node getSubmitsRootTable(Properties parameters, Node document) throws XPathExpressionException {
        String query = parameters.getProperty("root");
        XPathExpression rootExp = xPath.compile(query);
        return ((Node) rootExp.evaluate(document, XPathConstants.NODE));
    }

    private NodeList getSubmitRows(Properties parameters, Node root) throws XPathExpressionException {
        String query = parameters.getProperty("submits");
        XPathExpression submitsExp = xPath.compile(query);
        return (NodeList) submitsExp.evaluate(root, XPathConstants.NODESET);
    }


    /**
     * Determinate is row corresponds to submit.
     * @param submitRow is table row with submit information
     * @param submit checking for corresponding
     * @param parameters is information about submiRow
     * @return is submit correspond to submitRow
     * @throws Exception if something wrong with input parameters
     */
    private boolean isCorrespondsToSubmit(Node submitRow, Submit submit, Properties parameters) throws Exception {
        String login = extractParameter(submitRow, parameters, "login");
        String problem = extractParameter(submitRow, parameters, "problem");
        logger.debug("Submit login parameter=[" + login + "]");
        logger.debug("Submit problem parameter=[" + problem + "]");
        return login.equalsIgnoreCase(parameters.getProperty("login")) && problem.equalsIgnoreCase(submit.getProblemPseudoId());
    }

    /**
     * Update submit usinng corresponding submitRow information
     * @param submitRow is table row with submit information
     * @param submit checking for corresponding
     * @param parameters is information about submiRow
     * @return is submit correspond to submitRow
     * @throws Exception if something wrong with input parameters
     */
    private String processSubmitResult(Node submitRow, Submit submit, Properties parameters) throws Exception {
        String status = extractParameter(submitRow, parameters, "result");
        logger.info("Find status on the remote server = " + status);
        status = convertToContesterStatus(status, parameters);
        if (status.equals(Constants.getTestingStatus())) {
            logger.info("Submit is running on the remote server, trying again...");
            return "recheck";
        }
        submit.setStatus(status);
        if (status.equals(Constants.getAcceptedStatus())) {
            submit.setAccepted(1);
        }
        if (!status.equals(Constants.getCompilationErrorStatus())) {
            if (!status.equals(Constants.getAcceptedStatus())) {
                int testNumber = (int)extractIntegerParameter(submitRow, parameters, "numtest");
                submit.setTestNumber(testNumber);
            }
            int time = (int)extractIntegerParameter(submitRow, parameters, "time");
            submit.setTime(time);
            int memory = (int)extractIntegerParameter(submitRow, parameters, "memory");
            submit.setMemory(memory);
        }
        return "ok";
    }

    /**
     * Checking status from remote Judge
     * @param submit, which testing
     * @throws Exception if impossible to check status for a long time.
     */
    void checkStatus(Submit submit) throws Exception {
        try {
            ParametersConfig config = new ParametersConfig(getStatusProperties());

            long delay = Long.parseLong(config.getParameters().getProperty("wait-before-millis",
                    "" + Constants.getDefaultWaitTimeBeforeCheckingStatus()));
            logger.info("Waiting before checking status (" + delay + " ms)...");
            Thread.sleep(delay);
            logger.info("Checking status...");

            Node document = getHtmlDocumentFromCurrentSession(config.getUrl());

            Properties parameters = config.getParameters();

            Node root = getSubmitsRootTable(parameters, document);
            logger.debug("Text inside submit table: " + root.getTextContent());
            if (root == null) {
                throw new Exception("Unable to find submits table");
            }
            NodeList submitRows = getSubmitRows(parameters, root);

            for(int i = 0; i < submitRows.getLength(); ++i) {
                Node submitRow = submitRows.item(i);
                logger.debug("Text inside submit row: " + submitRow.getTextContent());

                if (isCorrespondsToSubmit(submitRow, submit, parameters)) {
                    String command = processSubmitResult(submitRow, submit, parameters);
                    if (command.equals("recheck")) {
                        checkStatus(submit);
                    }
                    return ;
                }
            }
            logger.info("Submit not found on the remote server");
            if (isTryOneMoreTime("checkStatus")) {
                logger.info("Trying again");
                checkStatus(submit);
            } else {
                throw new Exception("No result of submit for a long time.");
            }
        } catch (Exception e) {
            logger.warn("Unable to check status", e);
            if (isTryOneMoreTime("checkStatus")) {
                logger.info("Trying again");
                checkStatus(submit);
            } else {
                throw(e);
            }
        }
    }

    /**
     * Convert remote judge status for local status.
     * @param result is remote status
     * @param parameters is properties of remote judge
     * @return local status
     */
    private String convertToContesterStatus(String result, Properties parameters) {
        String keyPrefix = "result.";
        for (String key : parameters.stringPropertyNames()) {
            if (key.startsWith(keyPrefix) && !key.endsWith("xpath")) {
                String contesterStatus = key.substring(keyPrefix.length());
                Pattern p = Pattern.compile(parameters.getProperty(key), Pattern.DOTALL | Pattern.MULTILINE);
                Matcher m = p.matcher(result);
                if (m.matches()) {
                    return contesterStatus;
                }
            }
        }
        return Constants.getUnknownErrorStatus();
    }

    /**
     * Extract parameter from submitRow by name.
     * This value can be constant value or some value parsed from submiRow.
     * @param submitRow submit information
     * @param parameters remote judge configuration
     * @param name of parameter
     * @return value of parameter
     * @throws Exception if unable to match parameter
     */
    private String extractParameter(Node submitRow, Properties parameters, String name) throws Exception {
        String valueParam = name + ".value",
               xpathParam = name + ".xpath",
               regexParam = name + ".regex";
        if (parameters.containsKey(valueParam)) {
            return parameters.getProperty(valueParam);        
        }
        String query = parameters.getProperty(xpathParam);
        logger.debug("Extracting parameter [" + name + "] with query \"" + query + "\"");
        XPathExpression expression = xPath.compile(query);

        /* Old way:
        Node valueNode = (Node)expression.evaluate(submitRow, XPathConstants.NODE);
        String value = valueNode.getTextContent().trim();
        */

        String value = ((String)expression.evaluate(submitRow, XPathConstants.STRING)).trim();
        if (value.isEmpty()) return value;
        if (parameters.containsKey(regexParam)) {
            Pattern p = Pattern.compile(parameters.getProperty(regexParam), Pattern.DOTALL | Pattern.MULTILINE);
            Matcher m = p.matcher(value);
            if (m.matches()) {
                int groupCount = m.groupCount();
                return m.group(groupCount);
            } else {
                throw new Exception("unable to match parameter " + name);
            }
        } else {
            return value;
        }
    }

    private long extractIntegerParameter(Node submitRow, Properties parameters, String name) throws Exception {
        String factorParam = name + ".factor";
        String value = extractParameter(submitRow, parameters, name).replaceAll("[^0-9\\.,]+", "");
        double factor = 1.0;
        if (parameters.containsKey(factorParam)) {
            factor = Double.parseDouble(parameters.getProperty(factorParam));
        }
        return Math.round(Double.parseDouble("0" + value) * factor);
    }

    public static void print(Node node, String indent) {
        System.out.println(indent+node.getNodeName());
        NodeList childs = node.getChildNodes();
        for (int i = 0;  i < childs.getLength(); ++i) {
            print(childs.item(i), indent+" ");
        }
    }

    /**
     * Generate PostMethod and fill all parameters on it using config.
     * @param config is configuration of form
     * @return PostMethod with filled form parameters 
     * @throws Exception if unable to generate method
     */
    private PostMethod getPostMethod(ParametersConfig config) throws Exception {
        PostMethod method = new PostMethod(config.getUrl());
        method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
                new DefaultHttpMethodRetryHandler(3, false));
        Properties parameters = config.getParameters();
        for(String key : parameters.stringPropertyNames()) {
            method.setParameter(key, parameters.getProperty(key));
        }
        return method;
    }

    /**
     * @return Properties for login form.
     * @throws IOException if unable to find configuration file.
     */
    private Properties getLoginProperties() throws IOException {
        String fileName = JudgeHelper.getLoginFormFileName(judgeName);
        if (fileName == null || fileName.equals("NONE")) {
            return null; // no login form.
        }
        Properties properties = new Properties();
        properties.load(GenericOnlineJudge.class.getResourceAsStream(fileName));
        return properties;
    }

    /**
     * @return Properties for submit form.
     * @throws IOException if unable to find configuration file.
     */
    private Properties getSubmitProperties() throws IOException {
        Properties properties = new Properties();
        properties.load(GenericOnlineJudge.class.getResourceAsStream(JudgeHelper.getSubmitFormFileName(judgeName)));
        return properties;
    }

    /**
     * @return Properties for status page.
     * @throws IOException if unable to find configuration file.
     */
    private Properties getStatusProperties() throws IOException {
        Properties properties = new Properties();
        properties.load(GenericOnlineJudge.class.getResourceAsStream(JudgeHelper.getStatusFormFileName(judgeName)));
        return properties;
    }

    /**
     * Judge submit and update all information in database.
     * @param submit is judging submit
     */
    @Override
    public void judge(Submit submit) {
        httpClient = Browser.newSession();
        logger.info("Starting judge submit " + submit.getId());
        super.judge(submit);
        try {
            judgeName = submit.getPseudoJudge();

            startRetryingSection("login");
            login();

            startRetryingSection("submit");
            submit(submit);

            startRetryingSection("checkStatus");
            checkStatus(submit);
        } catch (Exception e) {
            logger.warn("Unable to submit solution, trying again...", e);
            if (isTryOneMoreTime("judge", Constants.getMaximalRetryingTime() * 5)) {
                judge(submit);
            } else {
                submit.setStatus(Constants.getGlobalErrorStatus());
            }
        }

        checkForSecondaryTimeLimit(submit);

        startRetryingSection("updateSubmitInformation");
        updateSubmitInformation(submit);
    }

    private void checkForSecondaryTimeLimit(Submit submit) {
        if (submit.getProblemProperties().containsKey("check-tl") && submit.getProblemProperties().get("check-tl").trim().length() > 0) {
            double timeLimitInSeconds = Double.parseDouble(submit.getProblemProperties().get("check-tl"));
            double timeLimit = timeLimitInSeconds * 1000.0;
            if (timeLimit < submit.getTime()) {
                logger.info("Seconary time limit exceeded detected");
                submit.setStatus("TL");
                submit.setTime((int) timeLimit);
            }
        }
    }

    private void updateSubmitInformation(Submit submit) {
        SubmitDao submitDao = new SubmitDaoImpl();
        submitDao.update(submit);
    }
}
